Un Packed Circle chart nos permite representar estructuras jerárquicas en el que cada círculo répresenta un nodo de la dimension jerárquica y el tamaño del círculo se correlaciona con un atributo o medida vinculada a ese nodo. Es muy util para estructuras jerárquicas, pero puede ser difícil comparar los tamaños de los círculos, especialmente cuando están densamente empaquetados o en diferentes niveles jerárquicos.
Se utiliza principalmente para representar datos cuantitativos y cualitativos.
Cargamos las librerías necesarías para la visualización de los datos.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import unicodedata
import circlify as circ
import warnings
warnings.filterwarnings('ignore')
Para nuestro juego de datos usaremos los datos disponibles en dades obertas de la Generalitat de Catalunya, en el cual se encuentran los presupuestos aprobados de la Generalitat de Catalunya para el ejercicio 2023.
# Cargamos el dataset
df_raw = pd.read_csv('Pressupostos_aprovats_de_la_Generalitat_de_Catalunya_20240415.csv')
df_raw.head()
| Subjecte/Àmbit | Exercici | Ingrés / Despesa | Servei / Entitat | Nom Servei / Entitat | Subsector | Codi Agrupació | Nom Agrupació | Codi Secció | Nom secció | ... | Aplicació | Nom Aplicació | Codi Àrea | Nom Àrea | Codi Política | Nom Política | Codi Programa | Nom Programa | Import sense consolidar | Import Consolidat Sector Públic | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | NaN | 2020 | D | AG01 | Gabinet i SG d'Ag.,Ram. Pesca i A | Generalitat | AG | Agricultura, Ramaderia, Pesca i Alimentació | AG | Agricultura, Ramaderia, Pesca i Alimentació | ... | 1000001 | Retribucions bàsiques | 1.0 | Funcionament de les institucions i administrac... | 12.0 | Administració i serveis generals | 121.0 | Direcció i administració generals | 410625.63 | 410625.63 |
| 1 | NaN | 2020 | D | AG01 | Gabinet i SG d'Ag.,Ram. Pesca i A | Generalitat | AG | Agricultura, Ramaderia, Pesca i Alimentació | AG | Agricultura, Ramaderia, Pesca i Alimentació | ... | 1000002 | Retribucions complementàries | 1.0 | Funcionament de les institucions i administrac... | 12.0 | Administració i serveis generals | 121.0 | Direcció i administració generals | 425228.38 | 425228.38 |
| 2 | NaN | 2020 | D | AG01 | Gabinet i SG d'Ag.,Ram. Pesca i A | Generalitat | AG | Agricultura, Ramaderia, Pesca i Alimentació | AG | Agricultura, Ramaderia, Pesca i Alimentació | ... | 1100001 | Retribucions bàsiques | 1.0 | Funcionament de les institucions i administrac... | 12.0 | Administració i serveis generals | 121.0 | Direcció i administració generals | 140482.42 | 140482.42 |
| 3 | NaN | 2020 | D | AG01 | Gabinet i SG d'Ag.,Ram. Pesca i A | Generalitat | AG | Agricultura, Ramaderia, Pesca i Alimentació | AG | Agricultura, Ramaderia, Pesca i Alimentació | ... | 1100002 | Retribucions complementàries | 1.0 | Funcionament de les institucions i administrac... | 12.0 | Administració i serveis generals | 121.0 | Direcció i administració generals | 365418.47 | 365418.47 |
| 4 | NaN | 2020 | D | AG01 | Gabinet i SG d'Ag.,Ram. Pesca i A | Generalitat | AG | Agricultura, Ramaderia, Pesca i Alimentació | AG | Agricultura, Ramaderia, Pesca i Alimentació | ... | 1200001 | Retribucions bàsiques | 1.0 | Funcionament de les institucions i administrac... | 12.0 | Administració i serveis generals | 121.0 | Direcció i administració generals | 24780580.69 | 24780580.69 |
5 rows × 31 columns
#normalizamos los nombres de las columnas
def to_ascii(text):
return ''.join(char if ord(char) < 128 else unicodedata.name(char)[:3] for char in text)
# Replace column names with ASCII equivalents
# df_raw.columns = [to_ascii(col) for col in df_raw.columns]
df_raw.columns = df_raw.columns.str.replace('/', '')
df_raw.columns = df_raw.columns.str.lower().str.replace(' ', '_')
df_raw.columns = df_raw.columns.str.lower().str.replace('__', '_')
# Visualizamos las columnas
print(df_raw.columns)
Index(['subjecteàmbit', 'exercici', 'ingrés_despesa', 'servei_entitat',
'nom_servei_entitat', 'subsector', 'codi_agrupació', 'nom_agrupació',
'codi_secció', 'nom_secció', 'tipus_de_secció',
'entitat_cp_sector_públic', 'ordre_departamental', 'codi_entitat',
'nom_entitat', 'capítol', 'nom_capítol', 'article', 'nom_article',
'concepte', 'nom_concepte', 'aplicació', 'nom_aplicació', 'codi_àrea',
'nom_àrea', 'codi_política', 'nom_política', 'codi_programa',
'nom_programa', 'import_sense_consolidar',
'import_consolidat_sector_públic'],
dtype='object')
Filtramos por las columnas que nos interesan para la visualización, en este caso seleccionamos las columnas de 'nom_agrupació', 'subsector' y 'import_consolidat_sector_públic' y filtramos por el ejercicio 2023 y por los valores de 'ingrés_despesa' que sean 'D' (Despesa).
df_work = df_raw.query('exercici == 2023 and ingrés_despesa == "D" and import_consolidat_sector_públic > 0')
# selecciona las columnas de nom_servei_entitat, nom_agració, nom_programa, import_consolidat_sector_públic)
df_clean = df_work[['nom_agrupació','subsector','import_consolidat_sector_públic']]
df_clean.head()
| nom_agrupació | subsector | import_consolidat_sector_públic | |
|---|---|---|---|
| 44 | Drets Socials | Generalitat | 30177165.22 |
| 110 | Interior | Generalitat | 500.00 |
| 125 | Drets Socials | Generalitat | 9676836.74 |
| 259 | Drets Socials | Generalitat | 5596898.03 |
| 272 | Drets Socials | Generalitat | 7500000.00 |
# numero de filas
print(f"Numero de filas: {df_clean.shape[0]: ,}")
Numero de filas: 11,893
Iniciamos la generación de las agregaciones.
# group by nom_servei_entitat, nom_agrupació, nom_programa and sum import_consolidat_sector_públic
df_grouped = df_clean.groupby(['nom_agrupació', 'subsector' ]).sum().reset_index()
# order by nom_servei_entitat, nom_agrupació, nom_programa
df_grouped = df_grouped.sort_values(['nom_agrupació', 'subsector' ])
df_grouped
| nom_agrupació | subsector | import_consolidat_sector_públic | |
|---|---|---|---|
| 0 | Acció Climàtica, Alimentació I Agenda Rural | Consorcis | 1.939577e+07 |
| 1 | Acció Climàtica, Alimentació I Agenda Rural | EA administratives i CatSalut | 3.872561e+06 |
| 2 | Acció Climàtica, Alimentació I Agenda Rural | Entitats dret públic | 1.148284e+09 |
| 3 | Acció Climàtica, Alimentació I Agenda Rural | Fundacions | 1.572711e+07 |
| 4 | Acció Climàtica, Alimentació I Agenda Rural | Generalitat | 9.591502e+08 |
| ... | ... | ... | ... |
| 62 | Territori | Entitats dret públic | 1.559871e+09 |
| 63 | Territori | Fundacions | 7.031770e+05 |
| 64 | Territori | Generalitat | 5.338767e+08 |
| 65 | Territori | Societats mercantils | 8.900330e+08 |
| 66 | Òrgans Superiors I Altres | Generalitat | 1.006592e+08 |
67 rows × 3 columns
# group by nom_servei_entitat, nom_agrupació, nom_programa and sum import_consolidat_sector_públic
df_grouped_2 = df_clean[['nom_agrupació','import_consolidat_sector_públic']].groupby(['nom_agrupació']).sum().reset_index()
# order by nom_servei_entitat, nom_agrupació, nom_programa
df_grouped_2 = df_grouped_2.sort_values(['nom_agrupació'])
df_grouped_2
| nom_agrupació | import_consolidat_sector_públic | |
|---|---|---|
| 0 | Acció Climàtica, Alimentació I Agenda Rural | 2.183375e+09 |
| 1 | Acció Exterior I Unió Europea | 9.724492e+07 |
| 2 | Cultura | 4.937219e+08 |
| 3 | Drets Socials | 3.405248e+09 |
| 4 | Economia I Hisenda | 8.791779e+08 |
| 5 | Educació | 6.742125e+09 |
| 6 | Empresa I Treball | 1.494817e+09 |
| 7 | Fons No Departamentals | 6.703634e+09 |
| 8 | Igualtat I Feminismes | 1.145687e+08 |
| 9 | Interior | 1.814303e+09 |
| 10 | Justícia, Drets I Memòria | 1.142574e+09 |
| 11 | Presidència | 1.838788e+09 |
| 12 | Recerca I Universitats | 1.763675e+09 |
| 13 | Salut | 1.223284e+10 |
| 14 | Territori | 4.794202e+09 |
| 15 | Òrgans Superiors I Altres | 1.006592e+08 |
# Initialize an empty list to store the dictionaries
tree_1 = dict()
# Iterate over each group
for row in df_grouped.itertuples(index=False):
# Create a dictionary for the parent
if row[0] not in tree_1.keys():
parent_dict = {
'id': row[0],
'children': [
{
'id': row[1],
'datum': row[2]
}
]
}
tree_1[row[0]] = parent_dict
else:
parent_dict = tree_1[row[0]]
parent_dict['children'].append({
'id': row[1],
'datum': row[2]
})
tree_2 = dict()
for row in df_grouped_2.itertuples(index=False):
if row[0] not in tree_2.keys():
parent_dict = {
'id': row[0],
'datum': row[1]
}
parent_dict['children'] = tree_1[row[0]]['children']
tree_2[row[0]] = parent_dict
else:
parent_dict = tree_1[row[0]]
parent_dict['children'].append({
'id': row[1],
'datum': row[2]
})
tree_3 = list()
# creamos el dict jerarquico final
for i,val in tree_2.items():
tree_3.append(val)
# Compute circle positions
circles = circ.circlify(
tree_3,
show_enclosure=False,
target_enclosure=circ.Circle(x=0, y=0, r=1)
)
fig, ax = plt.subplots(figsize=(14, 14))
# Title
ax.set_title('Distribución de los presupuestos de la Generalitat de Catalunya por Agrupación y Subsector')
# Remove axes
ax.axis('off')
# Find axis boundaries
lim = max(
max(
abs(circle.x) + circle.r,
abs(circle.y) + circle.r,
)
for circle in circles
)
plt.xlim(-lim, lim)
plt.ylim(-lim, lim)
# Print circle the highest level (subsector
for circle in circles:
if circle.level != 1:
continue
x, y, r = circle
ax.add_patch(plt.Circle((x, y), r, alpha=0.5,
linewidth=2, color="lightblue"))
# Print circle and labels for the highest level:
for circle in circles:
if circle.level != 2:
continue
x, y, r = circle
label = circle.ex["id"]
ax.add_patch(plt.Circle((x, y), r, alpha=0.5,
linewidth=2, color="#69b3a2"))
plt.annotate(label, (x, y), ha='center', color='white')
# Print labels for the continents
for circle in circles:
if circle.level != 1:
continue
x, y, r = circle
label = circle.ex["id"]
plt.annotate(label, (x, y), va='center', ha='center', bbox=dict(
facecolor='white', edgecolor='black', boxstyle='round', pad=0.005))
De la misma forma que la gráfica anterior, los Sunburst chart nos permiten representar jerarquías anidadas, pero en este caso forma de círculos concéntricos. Los círculos más externos representan la jerarquía de nivel superior, mientras que los círculos internos representan los niveles jerárquicos inferiores. Cada nivel de jerarquía se codifica por colores y el tamaño de los círculos se correlaciona con un atributo o medida vinculada a ese nodo.
De forma también similar a los Packed Circle Chart, resulta difícil comparar los tamaños de los círculos, especialmente cuando son muy densos y también es difícil etiquetar los nodos en estos casos
Para la visualización de los datos, utilizaremos los mismos datos que en la visualización anterior, pero en este caso utilizaremos columnas diferentes para la jerarquía.
# selecciona las columnas de nom_servei_entitat, nom_agració, nom_programa, import_consolidat_sector_públic)
df_clean_2 = df_work[['nom_agrupació','nom_capítol','import_consolidat_sector_públic']]
df_clean_2.head()
| nom_agrupació | nom_capítol | import_consolidat_sector_públic | |
|---|---|---|---|
| 44 | Drets Socials | Transferències de capital | 30177165.22 |
| 110 | Interior | Despeses corrents de béns i serveis | 500.00 |
| 125 | Drets Socials | Despeses corrents de béns i serveis | 9676836.74 |
| 259 | Drets Socials | Transferències de capital | 5596898.03 |
| 272 | Drets Socials | Transferències de capital | 7500000.00 |
import plotly.express as px
import plotly.graph_objects as go
# group by nom_servei_entitat, nom_agrupació, nom_programa and sum import_consolidat_sector_públic
df_grouped_3 = df_clean_2.groupby(['nom_agrupació', 'nom_capítol' ]).sum().reset_index()
# order by nom_servei_entitat, nom_agrupació, nom_programa
df_grouped_3 = df_grouped_3.sort_values(['nom_agrupació', 'nom_capítol' ])
df_grouped_3['año'] = 2023
df_grouped_3
| nom_agrupació | nom_capítol | import_consolidat_sector_públic | año | |
|---|---|---|---|---|
| 0 | Acció Climàtica, Alimentació I Agenda Rural | Aportacions de capital i préstecs | 4.924159e+07 | 2023 |
| 1 | Acció Climàtica, Alimentació I Agenda Rural | Despeses corrents de béns i serveis | 6.732715e+08 | 2023 |
| 2 | Acció Climàtica, Alimentació I Agenda Rural | Despeses de personal | 2.861530e+08 | 2023 |
| 3 | Acció Climàtica, Alimentació I Agenda Rural | Deute | 3.360370e+07 | 2023 |
| 4 | Acció Climàtica, Alimentació I Agenda Rural | Interessos | 6.566419e+06 | 2023 |
| ... | ... | ... | ... | ... |
| 110 | Òrgans Superiors I Altres | Aportacions de capital i préstecs | 7.420000e+04 | 2023 |
| 111 | Òrgans Superiors I Altres | Despeses corrents de béns i serveis | 1.734639e+07 | 2023 |
| 112 | Òrgans Superiors I Altres | Despeses de personal | 6.714821e+07 | 2023 |
| 113 | Òrgans Superiors I Altres | Inversions | 3.175329e+06 | 2023 |
| 114 | Òrgans Superiors I Altres | Transferències corrents | 1.291511e+07 | 2023 |
115 rows × 4 columns
# Creamos la figura de sunburst
fig = px.sunburst(df_grouped_3, path=['año','nom_agrupació', 'nom_capítol'], values='import_consolidat_sector_públic', color='nom_agrupació', hover_data=['import_consolidat_sector_públic'], title='Distribución de los presupuestos de la Generalitat por Agrupación y Capítulo para el ejercicio 2023')
# Actualizamos el diseño para ajustar el margen
fig.update_layout(
margin=dict(t=10, l=10, r=10, b=10),
)
# Actualizamos el texto para que sea radial
fig.update_traces(
textinfo='label+percent entry',
insidetextorientation='radial',
)
fig.show()
Un Ridgeline plot muestra la distribución de un valor numérico en diferentes grupos. Esta distribución puede ser utílizada mediante histogramas o gráficos de densidad, alineados en la misma escala horizontal con una ligera superposición.
Este gráfico es apropiado cuando existe un patron claro, ya que oculta parte la información que se superpone.
Para este caso, usaremos el set de datos de la cantidad de agua en los embalses internos de catalunya desde el 2020 hasta el 2000, disponible en dades obertes de la Generalitat de Catalunya.
# Cargamos el dataset
df_raw = pd.read_csv('Quantitat_d_aigua_als_embassaments_de_les_Conques_Internes_de_Catalunya_20240422.csv')
df_raw.head()
| Dia | Estació | Nivell absolut (msnm) | Percentatge volum embassat (%) | Volum embassat (hm3) | |
|---|---|---|---|---|---|
| 0 | 21/04/2024 | Embassament de Darnius Boadella (Darnius) | 132.40 | 11.5 | 7.03 |
| 1 | 21/04/2024 | Embassament de Riudecanyes | 194.74 | 2.8 | 0.15 |
| 2 | 21/04/2024 | Embassament de Susqueda (Osor) | 304.42 | 24.0 | 56.02 |
| 3 | 21/04/2024 | Embassament de Sant Ponç (Clariana de Cardener) | 514.45 | 32.6 | 7.94 |
| 4 | 21/04/2024 | Embassament de la Llosa del Cavall (Navès) | 769.31 | 20.4 | 16.30 |
# Nomralizamos los nombres de las columnas
df_raw.columns = ['dia','estacion','nivel_abs','porcentaje_volumen','volumen_embasado_hm3']
# Cast dia to datetime
df_raw['dia'] = pd.to_datetime(df_raw['dia'], format='%d/%m/%Y')
# Visualizamos las columnas
print(df_raw.columns)
print(f"Numero de filas: {df_raw.shape[0]: ,}")
Index(['dia', 'estacion', 'nivel_abs', 'porcentaje_volumen',
'volumen_embasado_hm3'],
dtype='object')
Numero de filas: 79,902
# get min and max date
min_date = df_raw['dia'].min()
max_date = df_raw['dia'].max()
print(f"Min date: {min_date}")
print(f"Max date: {max_date}")
Min date: 2000-01-01 00:00:00 Max date: 2024-04-21 00:00:00
# Filtramos los datos desde el 2010
df_work = df_raw.query('dia >= "01/01/2013" and dia <= "31/12/2023"')
min_date = df_work['dia'].min()
max_date = df_work['dia'].max()
print(f"Min date: {min_date}")
print(f"Max date: {max_date}")
df_work
Min date: 2013-01-01 00:00:00 Max date: 2023-12-31 00:00:00
| dia | estacion | nivel_abs | porcentaje_volumen | volumen_embasado_hm3 | |
|---|---|---|---|---|---|
| 1008 | 2023-12-31 | Embassament de la Baells (Cercs) | 593.56 | 22.2 | 24.35 |
| 1009 | 2023-12-31 | Embassament de Susqueda (Osor) | 300.21 | 20.4 | 47.43 |
| 1010 | 2023-12-31 | Embassament de Foix (Castellet i la Gornal) | 97.32 | 53.8 | 2.01 |
| 1011 | 2023-12-31 | Embassament de Sau (Vilanova de Sau) | 380.85 | 8.0 | 13.15 |
| 1012 | 2023-12-31 | Embassament de la Llosa del Cavall (Navès) | 766.56 | 17.7 | 14.18 |
| ... | ... | ... | ... | ... | ... |
| 37156 | 2013-01-01 | Embassament de Riudecanyes | 213.44 | 59.8 | 3.18 |
| 37157 | 2013-01-01 | Embassament de Darnius Boadella (Darnius) | 146.87 | 44.8 | 27.37 |
| 37158 | 2013-01-01 | Embassament de Susqueda (Osor) | 331.29 | 57.9 | 134.81 |
| 37159 | 2013-01-01 | Embassament de Sau (Vilanova de Sau) | 405.27 | 44.4 | 73.43 |
| 37160 | 2013-01-01 | Embassament de Siurana (Cornudella de Montsant) | 481.25 | 74.0 | 9.04 |
36153 rows × 5 columns
# creamos una columna para el año
df_work['año'] = pd.to_datetime(df_work['dia']).dt.year
df_work.head()
| dia | estacion | nivel_abs | porcentaje_volumen | volumen_embasado_hm3 | año | |
|---|---|---|---|---|---|---|
| 1008 | 2023-12-31 | Embassament de la Baells (Cercs) | 593.56 | 22.2 | 24.35 | 2023 |
| 1009 | 2023-12-31 | Embassament de Susqueda (Osor) | 300.21 | 20.4 | 47.43 | 2023 |
| 1010 | 2023-12-31 | Embassament de Foix (Castellet i la Gornal) | 97.32 | 53.8 | 2.01 | 2023 |
| 1011 | 2023-12-31 | Embassament de Sau (Vilanova de Sau) | 380.85 | 8.0 | 13.15 | 2023 |
| 1012 | 2023-12-31 | Embassament de la Llosa del Cavall (Navès) | 766.56 | 17.7 | 14.18 | 2023 |
# group by año and mean porcentaje_volumen
año_temp_serie = df_work.groupby(['año'])['porcentaje_volumen'].mean()
año_temp_serie
año 2013 82.810837 2014 83.002314 2015 80.212664 2016 65.626260 2017 64.983775 2018 69.862527 2019 74.700609 2020 87.505647 2021 73.161096 2022 46.492968 2023 25.543105 Name: porcentaje_volumen, dtype: float64
df_work['media_porcentaje_volumen'] = df_work['año'].map(año_temp_serie)
df_work.head()
| dia | estacion | nivel_abs | porcentaje_volumen | volumen_embasado_hm3 | año | media_porcentaje_volumen | |
|---|---|---|---|---|---|---|---|
| 1008 | 2023-12-31 | Embassament de la Baells (Cercs) | 593.56 | 22.2 | 24.35 | 2023 | 25.543105 |
| 1009 | 2023-12-31 | Embassament de Susqueda (Osor) | 300.21 | 20.4 | 47.43 | 2023 | 25.543105 |
| 1010 | 2023-12-31 | Embassament de Foix (Castellet i la Gornal) | 97.32 | 53.8 | 2.01 | 2023 | 25.543105 |
| 1011 | 2023-12-31 | Embassament de Sau (Vilanova de Sau) | 380.85 | 8.0 | 13.15 | 2023 | 25.543105 |
| 1012 | 2023-12-31 | Embassament de la Llosa del Cavall (Navès) | 766.56 | 17.7 | 14.18 | 2023 | 25.543105 |
# get unique years in df
years = df_work['año'].unique()
#sort years in asc order
years = np.sort(years)[::1].tolist()
years
[2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023]
# we generate a color palette with Seaborn.color_palette()
pal = sns.color_palette(n_colors=10)
# in the sns.FacetGrid class, the 'hue' argument is the one that is the one that will be represented by colors with 'palette'
g = sns.FacetGrid(df_work, row='año', hue='media_porcentaje_volumen', aspect=15, height=0.75, palette=pal)
# then we add the densities kdeplots for each month
g.map(sns.kdeplot, 'porcentaje_volumen',
bw_adjust=1, clip_on=False,
fill=True, alpha=1, linewidth=1.5)
# here we add a horizontal line for each plot
g.map(plt.axhline, y=0,
lw=2, clip_on=False)
plt.setp(ax.get_xticklabels(), fontsize=15, fontweight='bold')
plt.xlabel('% de volumen de embalses', fontweight='bold', fontsize=15)
g.fig.suptitle('Media diaria del % de volumen de embalses por año',
ha='right',
fontsize=20,
fontweight=20)
plt.show()